package org.hepeng.workx.spring.security.config;

import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.hepeng.workx.exception.ApplicationRuntimeException;
import org.joor.Reflect;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.util.Assert;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * @author he peng
 */
public class SecurityExpressionUrlAuthorizationConfigHelper {

    private static final Reflect EXPRESSION_URL_AUTHORIZATION_CONFIGURER_REFLECT = Reflect.on(ExpressionUrlAuthorizationConfigurer.class);
    public static final String PERMIT_ALL = EXPRESSION_URL_AUTHORIZATION_CONFIGURER_REFLECT.get("permitAll");
    public static final String DENY_ALL = EXPRESSION_URL_AUTHORIZATION_CONFIGURER_REFLECT.get("denyAll");
    public static final String ANONYMOUS = EXPRESSION_URL_AUTHORIZATION_CONFIGURER_REFLECT.get("anonymous");
    public static final String AUTHENTICATED = EXPRESSION_URL_AUTHORIZATION_CONFIGURER_REFLECT.get("authenticated");
    public static final String FULLY_AUTHENTICATED = EXPRESSION_URL_AUTHORIZATION_CONFIGURER_REFLECT.get("fullyAuthenticated");
    public static final String REMEMBER_ME = EXPRESSION_URL_AUTHORIZATION_CONFIGURER_REFLECT.get("rememberMe");
    public static final String NOT = "!";
    public static final String IGNORING = "web_security_ignoring";

    private boolean invoked;
    private boolean isNot;

    static {
    }

    private StringBuilder access = new StringBuilder();

    public List<ConfigAttribute> parse(Object obj) {
        Assert.notNull(obj , "obj must not be null");
        Map<Class<? extends Annotation>, Field[]> fields = parseFields(obj);

        if (MapUtils.isEmpty(fields)) {
            throw new ApplicationRuntimeException("the specified object could not be parsed");
        }

        Object val = null;
        Class<? extends Annotation> annotationClass = null;
        int notNullFieldCount = 0;

        try {
            for (Map.Entry<Class<? extends Annotation>, Field[]> entry : fields.entrySet()) {
                Field[] fieldArray = entry.getValue();
                if (ArrayUtils.isNotEmpty(fieldArray)) {
                    for (Field field : fieldArray) {
                        field.setAccessible(true);
                        Object tmpVal = field.get(obj);
                        if (Objects.nonNull(field.get(obj))) {
                            if ((Number.class.isAssignableFrom(tmpVal.getClass())
                                    && (byte) 0 == NumberUtils.toByte(tmpVal.toString()))
                                    || ((tmpVal instanceof String) && StringUtils.isNotBlank((CharSequence) tmpVal))) {
                                annotationClass = entry.getKey();
                                notNullFieldCount++;
                                val = tmpVal;
                            }
                        }
                    }
                }
            }

            Field[] notFields = fields.get(Not.class);
            if (ArrayUtils.isNotEmpty(notFields)) {
                Field notField = notFields[0];
                notField.setAccessible(true);
                if (Objects.nonNull(notField.get(obj))) {
                    not();
                }
            }
        } catch (Exception e) {
            throw new ApplicationRuntimeException(e);
        }

        if (notNullFieldCount > 1) {
            throw new ApplicationRuntimeException("spring security expression url authorization api can only be called once");
        }

        if (Objects.isNull(annotationClass)) {
            throw new ApplicationRuntimeException("the specified object could not be parsed");
        }

        if (Anonymous.class.isAssignableFrom(annotationClass)) {
            anonymous();
        } else if (Authenticated.class.isAssignableFrom(annotationClass)) {
            authenticated();
        } else if (DenyAll.class.isAssignableFrom(annotationClass)) {
            denyAll();
        } else if (FullyAuthenticated.class.isAssignableFrom(annotationClass)) {
            fullyAuthenticated();
        } else if (HasAnyAuthority.class.isAssignableFrom(annotationClass)) {
            Field[] hasAnyAuthorityFields = fields.get(HasAnyAuthority.class);
            Field hasAnyAuthorityField = hasAnyAuthorityFields[0];
            hasAnyAuthorityField.setAccessible(true);
            HasAnyAuthority hasAnyAuthority = hasAnyAuthorityField.getAnnotation(HasAnyAuthority.class);
            String[] anyAuthority = StringUtils.split((String) val, hasAnyAuthority.separatorChars());
            hasAnyAuthority(anyAuthority);
        } else if (HasAnyRole.class.isAssignableFrom(annotationClass)) {
            Field[] hasAnyRoleFields = fields.get(HasAnyRole.class);
            Field hasAnyAuthorityField = hasAnyRoleFields[0];
            hasAnyAuthorityField.setAccessible(true);
            HasAnyRole hasAnyRole = hasAnyAuthorityField.getAnnotation(HasAnyRole.class);
            String[] anyRole = StringUtils.split((String) val, hasAnyRole.separatorChars());
            hasAnyRole(anyRole);
        } else if (HasAuthority.class.isAssignableFrom(annotationClass)) {
            hasAuthority((String) val);
        } else if (HasIpAddress.class.isAssignableFrom(annotationClass)) {
            hasIpAddress((String) val);
        } else if (HasRole.class.isAssignableFrom(annotationClass)) {
            hasRole((String) val);
        } else if (PermitAll.class.isAssignableFrom(annotationClass)) {
            permitAll();
        } else if (RememberMe.class.isAssignableFrom(annotationClass)) {
            rememberMe();
        } else if (Ignoring.class.isAssignableFrom(annotationClass)) {
            ignoring();
        }

        return access();
    }

    private Map<Class<? extends Annotation> , Field[]> parseFields(Object obj) {
        Field[] anonymousFields = FieldUtils.getFieldsWithAnnotation(obj.getClass(), Anonymous.class);
        checkAnnotationFiledQuantity(Anonymous.class , anonymousFields);

        Field[] authenticatedFields = FieldUtils.getFieldsWithAnnotation(obj.getClass(), Authenticated.class);
        checkAnnotationFiledQuantity(Authenticated.class , authenticatedFields);

        Field[] denyAllFields = FieldUtils.getFieldsWithAnnotation(obj.getClass(), DenyAll.class);
        checkAnnotationFiledQuantity(DenyAll.class , denyAllFields);

        Field[] fullyAuthenticatedFields = FieldUtils.getFieldsWithAnnotation(obj.getClass(), FullyAuthenticated.class);
        checkAnnotationFiledQuantity(FullyAuthenticated.class , fullyAuthenticatedFields);

        Field[] hasAnyAuthorityFields = FieldUtils.getFieldsWithAnnotation(obj.getClass(), HasAnyAuthority.class);
        checkAnnotationFiledQuantity(HasAnyAuthority.class , hasAnyAuthorityFields);

        Field[] hasAnyRoleFields = FieldUtils.getFieldsWithAnnotation(obj.getClass(), HasAnyRole.class);
        checkAnnotationFiledQuantity(HasAnyRole.class , hasAnyRoleFields);

        Field[] hasAuthorityFields = FieldUtils.getFieldsWithAnnotation(obj.getClass(), HasAuthority.class);
        checkAnnotationFiledQuantity(HasAuthority.class , hasAuthorityFields);

        Field[] hasIpAddressFields = FieldUtils.getFieldsWithAnnotation(obj.getClass(), HasIpAddress.class);
        checkAnnotationFiledQuantity(HasIpAddress.class , hasIpAddressFields);

        Field[] hasRoleFields = FieldUtils.getFieldsWithAnnotation(obj.getClass(), HasRole.class);
        checkAnnotationFiledQuantity(HasRole.class , hasRoleFields);

        Field[] notFields = FieldUtils.getFieldsWithAnnotation(obj.getClass(), Not.class);
        checkAnnotationFiledQuantity(Not.class , notFields);

        Field[] permitAllFields = FieldUtils.getFieldsWithAnnotation(obj.getClass(), PermitAll.class);
        checkAnnotationFiledQuantity(PermitAll.class , permitAllFields);

        Field[] rememberMeFields = FieldUtils.getFieldsWithAnnotation(obj.getClass(), RememberMe.class);
        checkAnnotationFiledQuantity(RememberMe.class , rememberMeFields);

        Field[] ignoringFields = FieldUtils.getFieldsWithAnnotation(obj.getClass(), Ignoring.class);
        checkAnnotationFiledQuantity(Ignoring.class , ignoringFields);

        Map<Class<? extends Annotation> , Field[]> fieldArrays = new HashMap<>();
        fieldArrays.put(Anonymous.class , anonymousFields);
        fieldArrays.put(Authenticated.class , authenticatedFields);
        fieldArrays.put(DenyAll.class , denyAllFields);
        fieldArrays.put(FullyAuthenticated.class , fullyAuthenticatedFields);
        fieldArrays.put(HasAnyAuthority.class , hasAnyAuthorityFields);
        fieldArrays.put(HasAnyRole.class , hasAnyRoleFields);
        fieldArrays.put(HasAuthority.class , hasAuthorityFields);
        fieldArrays.put(HasIpAddress.class , hasIpAddressFields);
        fieldArrays.put(HasRole.class , hasRoleFields);
        fieldArrays.put(PermitAll.class , permitAllFields);
        fieldArrays.put(RememberMe.class , rememberMeFields);
        fieldArrays.put(Ignoring.class , ignoringFields);

        return fieldArrays;
    }

    private void checkAnnotationFiledQuantity(Class<? extends Annotation> annotationClass , Field[] fields) {
        if (ArrayUtils.getLength(fields) > 1) {
            throw new ApplicationRuntimeException("@" + annotationClass + " must be unique in the class");
        }
    }

    public SecurityExpressionUrlAuthorizationConfigHelper ignoring() {
        checkInvoked();
        this.access.append(IGNORING);
        invoked = true;
        return this;
    }

    public SecurityExpressionUrlAuthorizationConfigHelper permitAll() {
        checkInvoked();
        this.access.append(PERMIT_ALL);
        invoked = true;
        return this;
    }

    public SecurityExpressionUrlAuthorizationConfigHelper denyAll() {
        checkInvoked();
        this.access.append(DENY_ALL);
        invoked = true;
        return this;
    }

    public SecurityExpressionUrlAuthorizationConfigHelper anonymous() {
        checkInvoked();
        this.access.append(ANONYMOUS);
        invoked = true;
        return this;
    }

    public SecurityExpressionUrlAuthorizationConfigHelper authenticated() {
        checkInvoked();
        this.access.append(AUTHENTICATED);
        invoked = true;
        return this;
    }

    public SecurityExpressionUrlAuthorizationConfigHelper fullyAuthenticated() {
        checkInvoked();
        this.access.append(FULLY_AUTHENTICATED);
        invoked = true;
        return this;
    }

    public SecurityExpressionUrlAuthorizationConfigHelper rememberMe() {
        checkInvoked();
        this.access.append(REMEMBER_ME);
        invoked = true;
        return this;
    }

    public SecurityExpressionUrlAuthorizationConfigHelper not() {
        if (! isNot) {
            this.access.append(NOT);
            isNot = true;
        }
        return this;
    }

    public SecurityExpressionUrlAuthorizationConfigHelper hasAnyRole(String ... authorities) {
        checkInvoked();
        Object args = authorities;
        String hasAnyRole = EXPRESSION_URL_AUTHORIZATION_CONFIGURER_REFLECT
                .call("hasAnyRole" , args).get();
        this.access.append(hasAnyRole);
        invoked = true;
        return this;
    }

    public SecurityExpressionUrlAuthorizationConfigHelper hasRole(String role) {
        checkInvoked();
        Assert.notNull(role, "role cannot be null");
        Object args = role;
        Object hasRole = EXPRESSION_URL_AUTHORIZATION_CONFIGURER_REFLECT.call("hasRole", args).get();
        this.access.append(hasRole);
        invoked = true;
        return this;
    }

    public SecurityExpressionUrlAuthorizationConfigHelper hasAnyAuthority(String... authorities) {
        checkInvoked();
        Object args = authorities;
        Object hasAnyAuthority = EXPRESSION_URL_AUTHORIZATION_CONFIGURER_REFLECT.call("hasAnyAuthority", args).get();
        this.access.append(hasAnyAuthority);
        invoked = true;
        return this;
    }

    public SecurityExpressionUrlAuthorizationConfigHelper hasAuthority(String authority) {
        checkInvoked();
        Object args = authority;
        Object hasAuthority = EXPRESSION_URL_AUTHORIZATION_CONFIGURER_REFLECT.call("hasAuthority", args).get();
        this.access.append(hasAuthority);
        invoked = true;
        return this;
    }

    public SecurityExpressionUrlAuthorizationConfigHelper hasIpAddress(String ipAddressExpression) {
        checkInvoked();
        Object args = ipAddressExpression;
        Reflect hasIpAddress = EXPRESSION_URL_AUTHORIZATION_CONFIGURER_REFLECT.call("hasIpAddress", args);
        this.access.append(hasIpAddress);
        invoked = true;
        return this;
    }

    public List<ConfigAttribute> access() {
        return SecurityConfig.createList(this.access.toString());
    }

    private void checkInvoked() {
        if (invoked) {
            throw new ApplicationRuntimeException("spring security expression url authorization api can only be called once");
        }
    }

}
